浏览器跨域
# 1.跨域
跨域是由于浏览器的同源策略限制,当一个请求 URL 的协议、域名、端口 中任意一个与当前 URL 不同即为跨域
# 2.同源策略
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能都会受到影响,会很容易受到 XSS、CSFR 等攻击。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现
同源策略限制内容:
- cookie、localStorage、IndexedDB 等存储性内容
- DOM 节点
- AIAX 请求不能发送
允许跨域加载资源的标签:
<img src="xxx"><link href="xxx"><script src="xxx"><iframe src="xxx">
# 3.跨域实现方式和原理
# 1.JSONP
原理:利用 script 标签没有跨域的限制,网页可以得到从其他来源动态加载的 JSON 数据
优点:简单兼容性好,可用于解决主流浏览器的跨域数据的访问问题
缺点:仅支持 get 方法,不安全可能会受到 XSS 攻击,服务器可能不支持 JSONP
前端传递一个 callback 参数给后端,后端返回一个用 callback 参数包裹住的 json 数据
后端:
const express = require('express')
const app = express()
const port = 3000
app.get('/jsonp', (req, res) => {
let callback = req.query.callback
res.send(`${callback}("我是服务端")`)
})
app.listen(port, () => console.log(`Server listening on port ${port}!`))
2
3
4
5
6
7
8
9
10
原生 js 实现 jsonp:
let url = 'http://localhost:3000/jsonp?callback=handler'
let script = document.createElement('script')
script.src = url
document.querySelector('head').appendChild(script)
function handler(data){
console.log(data)// 我是服务端
}
/*利用promise封装*/
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function(data) {
resolve(data)
}
params = { ...params, callback }
console.log(params)
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
console.log(script.src)
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/jsonp',
params: { id: 1, name: 'cyy' },
callback: 'handler'
}).then(data => {
console.log(data)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
JQuery 的 JSONP 形式
$.ajax({
url:"http://localhost:3000/jsonp?callback=handler",
dataType:"jsonp",
type:"get",
jsonpCallback:"handler",// 自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",// 传递函数名的形参callback,可省略
success:function (data){
console.log(data);
}
});
2
3
4
5
6
7
8
9
10
# 2.CORS(Cross-Origin Resource Sharing,跨域资源共享)
CORS 需要浏览器和服务端同时支持,浏览器会自动进行 CORS 通信,服务端设置 Access-Control-Allow-Origin 就可以开启 CORS,该属性表示哪些域名可以访问资源
后端:
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:1105')
res.json({name: 'cyy'})
})
app.listen(port, () => console.log(`Server listening on port ${port}!`))
2
3
4
5
6
7
8
9
10
前端:
$.ajax({
url: 'http://localhost:3000/',
type: 'GET',
dataType: 'json',
success: function (data){
console.log(data)// {name: 'cyy'}
}
})
2
3
4
5
6
7
8
CORS 简单请求和非简单请求
简单请求
只有同时满足以下两种条件,就属于简答请求:
- 请求方法是以下三种之一:
- HEAD
- GET
- POST
- HTTP 的头信息不超过以下几种字段:
- Accept(告诉服务端,客户端接收的响应的类型)
- Accept-Language(客户端支持的语言)
- Content-Language(请求报文使用的语言)
- Last-Event-ID
- Content-Type(请求报文体的类型):只限于三个值
- application/x-www-form-urlencoded:表单默认的提交数据的格式
- multipart/form-data:表单中上传文件时的格式
- text/plain:纯文本格式
简单请求服务器返回的响应头信息:
Access-Control-Allow-Origin:值为请求时 origin 字段的值或者是
*,表示接受任意域名的请求Access-Control-Allow-Credentials:表示是否允许发送 cookie ,默认情况下,cookie 不包含在 CORS 请求之中
同时,需要在 Ajax 请求中开启
withCredentials属性var xhr = new XMLHttpRequest(); xhr.withCredentials = true;1
2
Access-Control-Expose-Headers:CORS请求时,
XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
非简单请求
非简单请求是对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type的类型是application/json
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)
预检请求
- 请求的头信息中的特殊字段
- Access-Control-Request-Method:浏览器 CORS 请求的 HTTP 方法
- Access-Control-Request-Headers:指定浏览器会额外发送的头信息字段
- 响应的头信息
- Access-Control-Allow-Methods:表示服务器支持的所有跨域请求的方法
- Access-Control-Allow-Headers:表示服务器支持的所有头信息字段
- Access-Control-Allow-Credentials
- Access-Control-Max-Age:指定本次预检请求的有效期
# 3.postMessage
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递
otherWindow.postMessage(message, targetOrigin[, transfer]);
- message: 将要发送到其他 window的数据。
- targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
- transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
//内嵌在http://localhost:3000/a.html
<script>
function load() {
let iframe = document.getElementById('iframe')
iframe.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //我不爱你
}
}
</script>
// b.html
<script>
window.onmessage = function(e) {
console.log(e.data) //我爱你
e.source.postMessage('我不爱你', e.origin)
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4.WebSocket
WebSocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器之间的全双工通信,同时也是跨域的一种解决方案,WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
原生WebSocket API使用起来不太方便,可以使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('我爱你');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
2
3
4
5
6
7
# 5.Node 中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,服务器向服务器请求不需要遵循同源策略
代理服务器,处理步骤:
- 接受客户端请求
- 将请求转发给服务器
- 拿到服务器响应数据
- 将数据转发给客户端
# 6.Nginx 反向代理
实现原理:类似于 Node 中间件代理,需要搭建一个 Nginx 中转服务器,用于转发请求
使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过 nginx 配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
先下载 nginx,然后将 nginx 目录下的 nginx.conf 修改如下:
// proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com;
#当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
7.window.name + iframe
8.location.hash + iframe
a 与 b 跨域通信,需要通过中间页 c 来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。a 传给 b,b 传给 c,c 再传给 a
具体实现:
在a中放一个回调函数,方便c回调。放一个iframe标签,随后传值
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
在b中监听哈希值改变,一旦改变,把a要接收的值传给c
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
2
3
4
5
6
7
8
9
在c中监听哈希值改变,一旦改变,调用a中的回调函数
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
2
3
4
5
6
7
9.document.domain
跨一级域名相同的域,例如 www.baidu.com 和 www.map.baidu.com 都有 baidu.com,它们之间的跨域访问可以通过设置相同的 document.domain 来进行